03. 优化技巧

本课技巧概述

下面是优化 C++ 代码所需的技巧概览。完成本课练习和最终项目时,你可以回来翻看本页。随着课程进展,所有技巧都会不断扩展。

如果你想知道我们为什么选择了这些技巧,那是因为它们都可用于优化 Andy 的直方图滤波器代码。这当然不是完整的优化技巧列表。它的意义在于,通过练习,你会从新的角度来思考你的代码。迄今为止,你的注意力都在代码编写上。现在,你需要观察你的编程决策所产生的后果了。

删除无用代码

写代码时,你可能会发现有一段代码已经不用了,但仍在程序中没有删除。要留意这种情况:记住,每行代码都会占用 CPU 资源,而且往往还伴随着内存读写操作。任何不必要的代码都会拖慢程序。

避免额外的 if 语句

带有多个分支的 if 语句通常比较低效:

int x = 5;
if (x >= 5) {x = x + 1;}
if (x < 5) {x = x - 1;}

你可以把上述代码转换为 if else 语句,这样可以避免 CPU 检查 x 是否小于 5。

避免嵌套的 for 循环

下面这段代码:

for (int i = 0; i < 5; i++) {
   for (int j = 0; j < 4; j++) {
         matrix[i][j];   
   } 
}

看起来有点眼熟。这是你之前在据这种迭代值的代码。有时候,你别无选择,只能使用嵌套 for 循环。但是,对于有些情况,如果换个思路,可能就不用使用嵌套 for 循环了。这样的话,你就不需要像上面那样进行 20 次迭代了,可能只需要 9 次迭代即可。

避免创建额外变量

你会看到,Andy 有时候创建了并不需要的变量。例如:

float x = 2;
float y = 7;
float z = 4;

float volume = x * y * z;

float volume_reciprocal = 1 / volume;

这里的 volume 变量并不是必需的。你可以直接计算倒数。

volume_reciprocal = 1 / (x * y * z);

这样可能不会大幅提升性能;单精度浮点变量在 C++ 中相对高效。但是,如果你创建了一个新变量,来容纳一个更大的变量,比如二维向量呢?所需的额外内存肯定或拖慢程序。

在内存中为向量保留空间

你之前一致在使用二维向量来表示矩阵。C++ 向量在便捷性上独具优势。你可以按需添加元素至向量。而数组则具备固定的长度,声明之后无法更改。

但这种灵活性是需要代价的。C++ 向量在执行时间方面非常低效。实际上,需要快速执行的程序通常都不会使用 C++ 向量,更不会使用很大的 if 循环来迭代向量。相反,这些程序在编写时一般会利用 CPU 或 GPU 上的 并行计算

当你声明并定义一个向量时,编译器会在内存中预留空间和一些额外字节,用于向量扩展。一旦向量长度扩展,超出了预留内存,整个向量就会被复制到 RAM 中的拥有足够空间的其他地方。

这非常低效!在 Andy 的直方图滤波器代码中,你已经知道了向量需要有多大,因为机器人的世界中网格空间的数量是固定的。如果为向量预留了空间,就可以避免向量长度扩展时重新分配额外内存。语法和编码一样简单:

std::vector<int> foo;
foo.reserve(15);

现在,foo 向量可以保证拥有足够的空间,可以容纳 15 个整数。

按引用传递变量

每次调用函数时,C++ 都会把所有输入变量复制到内存中,无论这些变量是否已经在内存中。对于基础的数据类型,如整数、字符、单精度浮点数,这可能不是问题。

但某些变量占用空间大得多,比如向量。这时候,赋值过程会拖慢程序。

你需要学会如何按引用传递变量,而非按值传递变量。按引用传递变量时,会告知你的函数直接使用内存中的变量,无需把整个变量再次复制到内存中。

使用静态变量

在 Andy 的代码中,你能看到,部分向量和值是在函数内部计算的。但这些值在每次调用函数时都是一样的。

为避免一再重复计算这些变量,你可以把这些变量声明为静态的。首次调用函数时,C++ 把这些值存储在内存里,然后每次调用函数时重用这些值即可。

课程后面部分

课程的后面部分包括一系列练习,让你为项目做好准备。每次练习中,你都会收到一段代码,其中包括本页讨论的一个问题。你需要修改代码,让程序运行更快。

你可以看到,每次运行代码时,计时器给出的结果稍有不同。运行 C++ 程序时,CPU 可能同时执行其他任务,这会影响计时。请务必多次执行代码,以确定你的代码运行速度是否更快。